package verifier;

import java.util.logging.Level;
import java.util.logging.Logger;

import utils.ByteTree;
import utils.ProtInfo;
import crypto.*;
import ArithmeticObjects.*;
import GeneralObjects.BooleanArray;
import GeneralObjects.CipherTextArray;
import utils.*;
import Proofs.*;

/** This class is the main class of the program, and it contains the Core Verification Algorithm.
 * 
 * @author Oded Raphael
 * @author Etai Plushnick
 */
public class Main {

	// private logger and output members
	private static boolean allowLogging = true;
	private static boolean verbose = false;
	private static Logger logger;

	private static void printLog(String msg,Level level){
		if (allowLogging) 
			logger.log(level, msg);
		if (verbose || level == Level.SEVERE) {
			{
				if (msg.charAt(0)=='-') System.out.println();
				System.out.println("Verifier-" + Logging.getDateTime() + ": " + level.getName() + ": " + msg);	
			}
		}
	}

	private static String makeHeader(String h) {
		String header = "-------------------------";
		header += h;
		for (int i=0; i<70-h.length(); i++)
			header += '-';
		return header;
	}

	private static ElementArray TDecrypt(CipherTextArray c, ElementArray f, int N, ModularGroup Gq) {
		ElementArray result = null; 
		try {
			result = new ModularGroupArray(Gq);
			for (int i=0; i<N; i++) 
				result.addElement(c.getCipherTextForIndex(i).getRightElement().divide(f.getElementForIndex(i)));
		}
		catch (Exception e){}
		return result;
	}

	public static void main(String[] args) {

		try {
			if ((args.length < 2) || (args.length > 3)){
				usage();
				return;
			}
			else if (args.length == 3) {
				if (args[2].equals("-v")) 
					verbose = true;
				else {
					usage();
					return;
				}
			}

			/* Define and create logger for program */

			logger = Logging.getLogger();
			if (logger == null){
				System.err.println("Error, no permission to write log file");
				allowLogging = false;
			}
			else {
				logger.setUseParentHandlers(false);
			}
			/* If args[2]=="-v", log messages will be printed on screen as well as in log file */

			printLog(makeHeader("Step 1: Commencing verification process"),Level.INFO);
			printLog("Done!",Level.INFO);
			/* Step 2 of core verification algorithm: Read public parameters from protocol info file */
			printLog(makeHeader("Step 2: Reading public parameters from protocol info file"),Level.INFO);
			/* Read the protocol info file.  If this fails program will exit */
			ProtInfo procInfo = new ProtInfo(args[0]); 

			/* Placement of parameters from protocol info file */
			int numOfParties = procInfo.getNumParties();
			int threshold = procInfo.getThreshold();
			int maxCiphers = procInfo.getMaxCiphers();
			int randomBits = procInfo.getNumRandomBits();
			int statError = procInfo.getStatError();
			int challengeBits = procInfo.getNumChallengeBits();
			HashFunction H = procInfo.getROHash();
			PRG prg = procInfo.getPrg();
			ModularGroup group = procInfo.getGroup();

			Element p = group.getP(); 
			Element q = group.getQ();

			printLog("Done!",Level.INFO);

			/* Step 3 of core verification algorithm: Computation of rho, the prefix to the random oracles */

			printLog(makeHeader("Step 3: Computing rho, the prefix to the random oracles"), Level.INFO);
			byte[] rho = H.digest(procInfo.getF());
			printLog("Done!",Level.INFO);

			/* Step 4 of core verification algorithm: Read the joint public key (g, y) from the proof directory. */

			/* Read the proof directory.  If this fails, program will exit. */
			printLog(makeHeader("Step 4: Reading contents of proof directory."), Level.INFO);
			RoProof proofDir = new RoProof(args[1], procInfo.getNumParties());
			printLog("Done!",Level.INFO);

			/* Retrieve public key. */
			printLog("Extracting public key from proof directory.", Level.INFO);
			/* Compare group of public key with group from .xml file.  If different, the program will exit. */
			ByteTree pubKeyGroupBt = proofDir.getFullPublicKey().getChild(0).getChild(1);
			ModularGroup pubKeyGroup = new ModularGroup(pubKeyGroupBt);
			if (pubKeyGroup.compareTo(group) != 0) {
				printLog("Bad public key: Group of public key is inconsistent with group from ProtInfo.xml", Level.SEVERE);
				return;
			}
			/* Extract public key from proof directory. */
			ByteTree pubKey = proofDir.getFullPublicKey().getChild(1); //Public key is second child of byte tree
			if (pubKey.getNumOfChildren()!= 2) { //pubKey should have two children: g and y
				printLog("Bad public key: Wrong number of children.",Level.SEVERE);
				return;
			}

			/* Extract g and y from public key */
			Element g = new ModularGroupElement(pubKey.getChild(0), group);
			Element y = new ModularGroupElement(pubKey.getChild(1), group);
			printLog("Done!",Level.INFO);

			/* Step 5 of core verification algorithm: Read individual public keys from proof directory, 
			 * and make sure their product is equal to y. */

			/* Retrieve partial public keys. If this fails, program will exit. */
			printLog(makeHeader("Step 5: Retrieving individual public keys from proof directory."),Level.INFO);
			ByteTree[] partialPubKeys=new ByteTree[numOfParties];
			for (int i = 1; i <= numOfParties; i++) {
				partialPubKeys[i-1] = proofDir.getPublicKey(i);
				if (partialPubKeys[i-1] == null) { //if null then partial key i-1 wasn't retrieved successfully.
					printLog("Error retrieving partial public key "+i+".",Level.SEVERE);
					return;
				}
			}

			/* Interpret partial public key byte trees as modular group elements. */
			Element[] py = new Element[numOfParties];
			for (int i = 1; i <= numOfParties; i++) 
				py[i-1] = new ModularGroupElement(partialPubKeys[i-1], group);

			printLog("Done!",Level.INFO);

			/* Verify that product of partial public keys is equal to y.  */
			printLog("Verifying relation between public keys",Level.INFO);
			Element product = py[0];
			for (int i = 1; i < numOfParties; i++)
				product = product.multiply(py[i]);
			if (product.compareTo(y) != 0) {
				printLog("Product of partial public keys is not equal to full public key.",Level.SEVERE);
				return;
			}
			printLog("Done!",Level.INFO);

			/* Step 6 of core verification algorithm: Read the zero-th array of N <= maxCiphers ciphertexts. */

			/* Read the zero-th ciphertext array from the proof directory. If this fails, program will exit. */
			printLog(makeHeader("Step 6: Reading ciphertexts from proof directory"),Level.INFO);
			ByteTree ciphertextsBt = proofDir.getCiphertextList(0); //'null' is placed if reading failed.
			if (ciphertextsBt == null) {
				printLog("Failed to read zero-th ciphertext array.",Level.SEVERE);
				return;
			}

			/* Interpret ciphertexts byte tree as an array of ciphertexts. */
			CipherTextArray LPrev = new CipherTextArray(ciphertextsBt, group); 
			int N = LPrev.getArraySize(); //N is the number of ciphertexts in LZero
			if ((N > maxCiphers) && (maxCiphers != 0)) { //N must be smaller or equal to maxCiphers
				printLog("Ciphertext array is larger than maximum number of ciphertexts.",Level.SEVERE);
				return;
			}
			if (maxCiphers == 0){ //Check if no pre-computation was made.
				maxCiphers = N;
			}
			printLog("Done!",Level.INFO);

			printLog("Derive independent generators",Level.INFO);

			/* Step 7 of core verification algorithm: Verification of proof of shuffles for each party below threshold. */
			printLog(makeHeader("Verifying proof of shuffles for each party below the threshold"),Level.INFO);
			/* Define random oracles used in proofs. */
			RandomOracle ROs = new RandomOracle(H, prg.getSeedLen()); //ROs is random oracle used for computation of seeds.
			RandomOracle ROv = new RandomOracle(H, challengeBits); //ROv is random oracle used for computation of challenges.

			/* Declaration of variables. */
			ByteTree uBT, tau, sigma;
			ElementArray uTemp, u;
			BooleanArray keepList;
			ByteTree keepListBt;
			boolean[] keepListTemp = new boolean[maxCiphers];
			ProofOfShuffle POS = new ProofOfShuffle(ROs, ROv);
			ProofOfCCPoS CCPOS = new ProofOfCCPoS(ROs, ROv);

			/* Step 7(a): Verification of proof of shuffle of commitments. */

			/* Computation of h, an array of independent generators used in verifications of proofs of shuffles. */ 
			int np = p.getValue().bitLength(); //np is the bit length of p
			byte[] seed = POS.computeSeed(rho); //seed, for now, is the seed used to compute h.
			Element[] hTemp = POS.computeRandomArray(seed, (np+statError), prg, maxCiphers, 
					new LargeNumberObject(np + statError), 
					p.subtract(new LargeNumberObject(LargeNumber.ONE)).divide(q), p);
			//hTemp is a regular array of modular group elements, before conversion to ModularGroupArray type.
			ElementArray h = new ModularGroupArray(hTemp, group); //Conversion to ModularGroupArray type.

			for (int i = 1; i <= threshold; i++) { //Verification is performed for each party under threshold.

				printLog(makeHeader("Verifying shuffle of party no. "+i+":"),Level.INFO);
				printLog("Extracting permutation commitment",Level.INFO);
				/* Extraction of commitments and reply from proof directory. 'null' is placed in case of failure. */
				uBT = proofDir.getPermutationCommitment(i); //uBT is the i-th permutation commitment
				tau = proofDir.getPoSCommitment(i); //tau is the i-th commitment of Fiat-Shamir proof of shuffle
				sigma = proofDir.getPoSReply(i); //sigma is the i-th reply of Fiat-Shamir proof
				/* If reading of any files failed, uTemp is defined as h. */
				if ((uBT == null) || (tau == null) || (sigma == null))
				{
					printLog("Failed!",Level.INFO);
					uTemp = h;
				}
				/* Otherwise, uTemp is the interpretation of uBT as a modular group array. */
				else
				{
					printLog("Done!",Level.INFO);
					uTemp = new ModularGroupArray(uBT, group);
				}

				/* Verification of proof of shuffle of commitments. */
				printLog("Verifying proof of shuffle of commitments",Level.INFO);
				if (!POS.verify(rho, maxCiphers, randomBits, statError, challengeBits, prg, group, uTemp, tau, sigma, h))
				{
					printLog("Failed!",Level.INFO);
					uTemp = h;  //If verification fails, uTemp is defined as h.
				}
				else
				{
					printLog("Done!",Level.INFO);
				}

				/* Step 7(b): Shrink permutation commitment. */
				printLog("Shrinking permutation commitment",Level.INFO);

				/* Read the keep-list from the proof directory. 'null' is placed in case of failure. */
				printLog("Reading keep-list from proof directory",Level.INFO);
				keepListBt = proofDir.getKeepList(i);

				/* If reading failed, keepList is defined as an array of maxCiphers booleans, in which the first
				 * N are 'true', and the rest are 'false'. */
				if (keepListBt == null) {
					printLog("Failed!",Level.INFO);
					for (int j=0; j<N; j++)
						keepListTemp[j]=true;
					for (int j=N; j<maxCiphers; j++)
						keepListTemp[j]=false;
					keepList = new BooleanArray(keepListTemp);
				} 
				/* Otherwise, keepList is the interpretation of keepListBt as a BooleanArray type. */
				else
				{
					printLog("Done!",Level.INFO);
					keepList = new BooleanArray(keepListBt);
				}

				/* Creation of the array u.  u contains uTemp[i] iff keepList[i] == true. */
				u = new ModularGroupArray(group);
				for (int j = 0; j < maxCiphers; j++)
				{
					if (keepList.getElementForIndex(j))
						u.addElement(uTemp.getElementForIndex(j));
				}

				/* Step 7(c): Read the i-th array of N ciphertexts from proof directory.  If this fails, program will exit. */
				printLog("Reading the party's ciphertexts array from the proof directory",Level.INFO);
				ByteTree LBt = proofDir.getCiphertextList(i); //'null' is placed if reading failed.
				if (LBt == null) {
					printLog("Failed to read ciphertext array number " + i + ".",Level.SEVERE);
					return;
				}
				CipherTextArray L = new CipherTextArray(LBt, group); //L is the interpretation of LBt as a ciphertext array.
				if (L.getArraySize() != N) { //Make sure L is of size N.
					printLog("Ciphertext array number " + i + " is wrong size.",Level.SEVERE);
					return;
				}
				
				printLog("Done!",Level.INFO);

				/* Step 7(d): Verification of commitment-consistent proof of shuffle of i-th party. */
				printLog("Verifying commitment-consistent proof of shuffle",Level.INFO);
				/* Read commitment-consistent commitment and reply. 'null' is placed in case of failure. */
				tau = proofDir.getCCPoSCommitment(i); //tau is the i-th commitment consistent proof commitment.
				sigma = proofDir.getCCPoSReply(i); //sigma is the i-th commitment consistent proof reply.
				if ((tau == null) || (sigma == null)) {
					if (L.compareTo(LPrev)!=0) //In case of failure, L should be equal to LPrev.
					{
						printLog("Inconsistent ciphertexts: Array " +
								"number " + i + " different from array number " +(i-1)+ ".",Level.SEVERE);
						return;
					}
				}

				/* Verification of commitment-consistent proof of shuffle of commitments. */
				printLog("Verifying commitment-consistent proof of shuffle of commitment",Level.INFO);
				if (!CCPOS.verify(rho, N, randomBits, statError, challengeBits, prg, group, u, pubKey, LPrev, L, tau, sigma, h))
				{
					printLog("Failed!",Level.INFO);
					if (L.compareTo(LPrev)!=0) //In case of failure, L should be equal to LPrev.
					{
						printLog("Inconsistent ciphertexts: Array " +
								"number " + i + " different from array number " +(i-1)+ ".",Level.SEVERE);
						return;
					}
				}
				else
				{
					printLog("Done!",Level.INFO);
				}

				LPrev = L;
			} //End of 'for' loop.

			/* Step 8 of core verification algorithm: Verification of proof of decryption for each party. */

			/* Declaration of variables. */
			ByteTree secretKeyBt;
			Element secretKey;
			ProofOfDecrypt PoD = new ProofOfDecrypt(ROs, ROv);
			ElementArray[] decryptionFactors = new ModularGroupArray[numOfParties];
			CipherTextArray LLast = new CipherTextArray(proofDir.getCiphertextList(threshold), group);

			for (int i=0; i<numOfParties; i++) {
				printLog(makeHeader("Verifying proof of decryption of party no. "+i+":"),Level.INFO);
				/* Step 8(a): Read i-th secret-key x[i], and verify that py[i] == g^x[i]. */

				/* Retrieval of i-1 ciphertext array, needed in case equality exists. */
				ByteTree LBt = proofDir.getCiphertextList(i);
				LPrev = new CipherTextArray(LBt, group);

				/* Read i-th secret key from proof directory.  'null' is placed in case of failure. */
				printLog("Reading the party's secret key",Level.INFO);
				secretKeyBt = proofDir.getSecretKey(i);

				/* In case of failure, go to step 8(b): Read decryption factor, commitment and reply of the i-th
				 * party, then perform verification of proof of decryption.*/

				if (secretKeyBt == null) {
					printLog("Secret key could not be read",Level.WARNING);
					/* If reading of decryption factor, commitment, or reply fails, the program will exit. */
					printLog("Reading decryption factors",Level.INFO);
					decryptionFactors[i] = new ModularGroupArray(proofDir.getDecryptionFactors(i).getChild(1), group);
					printLog("Done!",Level.INFO);
					printLog("Reading decryption factor commitment",Level.INFO);
					tau = proofDir.getDecrFactCommitment(i);
					sigma = proofDir.getDecrFactReply(i);
					printLog("Done!",Level.INFO);

					/* Verification of proof of decryption. If verification is unsuccessful, program will exit. */
					printLog("Verifying proof of decryption",Level.INFO);
					if (!PoD.verify(rho, N, randomBits, statError, challengeBits, prg, group, py[i], LLast, 
							decryptionFactors[i], tau, sigma)) {
						printLog("Proof of decryption was unsuccessful.",Level.SEVERE);
						return;
					}
					else
					{
						printLog("Done!",Level.INFO);
					}
				} else { //secretKeyBt was read successfully.
					printLog("Done!",Level.INFO);
					secretKey = new LargeNumberObject(secretKeyBt);
					if (py[i].compareTo(g.power(secretKey))!=0) //Check if py[i] == g^x[i]
					{
						/* In case of inequality, go to step 8(b): Read decryption factor, commitment and reply of the i-th
						 * party, then perform verification of proof of decryption.*/
						printLog("Reading decryption factors",Level.INFO);
						decryptionFactors[i] = new ModularGroupArray(proofDir.getDecryptionFactors(i), group);
						printLog("Done!",Level.INFO);
						printLog("Reading decryption factor commitment",Level.INFO);
						tau = proofDir.getDecrFactCommitment(i);
						sigma = proofDir.getDecrFactReply(i);
						printLog("Done!",Level.INFO);
						printLog("Verifying proof of decryption",Level.INFO);
						if (!PoD.verify(rho, N, randomBits, statError, challengeBits, prg, group, py[i], LLast, 
								decryptionFactors[i], tau, sigma)) {
							printLog("Proof of decryption was unsuccessful.",Level.SEVERE);
							return;
						}
						else
						{
							printLog("Done!",Level.INFO);
						}
					} else { //secretKeyBt was read successfully, and equality exists.
						for (int j=0; j<maxCiphers; j++)
							decryptionFactors[i].addElement(PoD.PDecrypt(secretKey, LPrev.getCipherTextForIndex(j)));
					}
				}
			} //end of 'for' loop.

			/* Step 9 of core verification algorithm: Verification of output. */
			printLog(makeHeader("Step 9: Verifying output"),Level.INFO);

			/* Read plaintext elements from proof directory.  If this fails, program will exit. */
			printLog("Reading plaintexts from proof directory",Level.INFO);
			ByteTree plainTextBt = proofDir.getPlainTextElements();

			/*Interpretation of plainTextBt as modular group array. */
			ElementArray plainText = new ModularGroupArray(plainTextBt, group);
			
			printLog("Done!",Level.INFO);

			/* Computation of product of decryption factors. */
			printLog("Computing decryption factors",Level.INFO);
			ElementArray jointDecryptionFactor = decryptionFactors[0];
			for (int i=1; i<numOfParties; i++){
				jointDecryptionFactor=jointDecryptionFactor.arrayMultiply(decryptionFactors[i]);
			}
			printLog("Done!",Level.INFO);

			/* Comparison of plainText to result of TDecrypt.  In case of inequality, program will exit. */
			printLog("Verifying plaintext",Level.INFO);
			if (plainText.compareTo(TDecrypt(LLast, jointDecryptionFactor, N, group))!=0) {
				printLog("Plain text array is not equal to trivial decryption.",Level.SEVERE);
				return;
			}
			printLog("Done!",Level.INFO);

			printLog("Verification was successful!",Level.INFO);

		}
		catch (Exception e) {
			e.printStackTrace();
			printLog(e.getMessage(),Level.SEVERE);
		}
	}

	private static void usage() {
		System.out.println("Error, wrong number of arguments to program");
		System.out.println("Allowed number of arguments is 2 or 3:");
		System.out.println("1. Path to protInfo.xml file (mandatory)");
		System.out.println("2. Path to roProof directory (mandatory)");
		System.out.println("3. '-v' flag to indicate verbose printing (optional)");
	}
}
